JDK1.8 新特性 · 复习手册
预计阅读 33 分钟

JDK1.8 新特性 · 复习手册

JDK1.8 新特性 · 复习手册

一、接口 & 函数式接口


卡片 1|default 方法 —— 为什么接口可以有方法体了?

Q:JDK8 为什么要给接口加 default 方法?它和抽象类有什么区别?

A:

JDK8 之前,接口只能声明抽象方法。这意味着:只要在接口中新增一个方法,所有实现类全部编译报错,必须逐一修改。这在大型项目(比如 JDK 自身的 Collection 接口)中完全不可接受。

default 方法的本质是在接口中提供一个带方法体的默认实现,实现类可以选择:

  • 不重写 → 自动继承默认实现
  • 重写 → 用自己的逻辑覆盖

典型场景:

Java
1// JDK 给 Collection 加了 stream(),如果不加 default,
2// 所有 List/Set/Queue 的子类全部要改,灾难级别。
3interface Collection<E> {
4    default Stream<E> stream() {
5        return StreamSupport.stream(spliterator(), false);
6    }
7}

和抽象类的区别:

接口 + default抽象类
继承数量可实现多个只能继承一个
成员变量只能 public static final可以有实例变量
构造函数不能有可以有
this 含义指向实现类实例指向自身实例

🧠 钩子: default = "给你默认实现,你不用必须改"。接口轻量,抽象类重量。


❌ 常见反例

Java
1// ❌ 错误:接口的 default 方法不能调用 Object 的方法(编译报错)
2interface Foo {
3    default String toString() {  // ❌ 编译错误!
4        return "Foo";
5    }
6}
7// 原因:Object 的 toString/hashCode/equals 是"最终仲裁者",
8// 接口 default 不允许覆盖它们,否则继承链混乱。
Java
1// ❌ 错误:两个接口有同名 default 方法,实现类必须手动解决冲突
2interface A { default void say() { System.out.println("A"); } }
3interface B { default void say() { System.out.println("B"); } }
4
5class C implements A, B {
6    // ❌ 编译错误!必须重写 say()
7    // ✅ 正确做法:
8    @Override
9    public void say() {
10        A.super.say();  // 显式指定调用哪个
11    }
12}

🔍 追问

Q1:接口的 static 方法能被实现类继承吗? A: 不能。接口的静态方法只能通过 接口名.方法() 调用,实现类不会继承它。这和类的静态方法继承规则不同。

Java
1interface MyInterface {
2    static void hello() { System.out.println("hello"); }
3}
4
5class MyClass implements MyInterface {
6    void test() {
7        // MyClass.hello();  // ❌ 编译错误
8        MyInterface.hello(); // ✅ 正确
9    }
10}

Q2:为什么接口的 default 方法不能是 finalsynchronized A: final 违背了"允许实现类重写"的初衷;synchronized 会引入状态依赖,但接口本身不持有状态(no instance fields),加锁没有意义且破坏接口的纯粹性。


卡片 2|函数式接口 —— 凭什么只准有一个抽象方法?

Q:什么是函数式接口?@FunctionalInterface 是不是必须的?

A:

函数式接口 = 有且仅有一个抽象方法的接口。这个限制为什么重要?因为 Lambda 表达式的语法本身就是"隐式实现那个唯一的抽象方法"——如果接口有两个抽象方法,编译器就不知道该实现哪个了。

@FunctionalInterface 不是必须的,它是一个编译期校验注解

  • 加上它 → 编译器帮你检查,违反规则直接编译报错
  • 不加它 → 也能当函数式接口用,但容易手滑加方法而不自知
Java
1// ✅ 合法(不加注解也能用于 Lambda)
2interface Converter {
3    String convert(Integer i);
4}
5
6// ✅ 推荐写法(加注解让编译器替你盯着)
7@FunctionalInterface
8interface Converter {
9    String convert(Integer i);
10    // 可以加 default 和 static 方法,不破坏"单一抽象方法"规则
11    default String defaultConvert(Integer i) { return String.valueOf(i); }
12}

🧠 钩子: 只有一个抽象方法 = 函数式接口;+ 注解 = 编译器帮你盯着。


❌ 常见反例

Java
1// ❌ 错误:两个抽象方法
2@FunctionalInterface
3interface Broken {         // ❌ 编译报错!
4    void doA();
5    void doB();
6}
7
8// ❌ 错误:但很容易不经意犯错——比如继承了一个抽象方法
9interface Base {
10    void doBase();
11}
12
13@FunctionalInterface
14interface Child extends Base {
15    void doChild();  // ❌ 现在有两个抽象方法了!(Base 的 + 自己的)
16}
Java
1// ✅ 正确:继承的方法如果也是抽象方法,总数超过 1 就不行
2// ✅ 但如果继承的是 default 方法,那就不算抽象方法
3@FunctionalInterface
4interface Good extends Base2 {
5    // Base2 里有一个 default 方法 → 不算抽象方法
6    // 所以这里只有一个抽象方法 → 合法
7    void doSomething();
8}

🔍 追问

Q1:RunnableCallable 是函数式接口吗? A: 都是。Runnable 只有 void run()Callable 只有 V call()。JDK 已在源码中给它们加了 @FunctionalInterface

Q2:函数式接口可以继承其他接口吗? A: 可以,但要保证继承后抽象方法总数 ≤ 1。如果父接口有 0 个抽象方法(全部是 default/static),子接口可以自己有 1 个;如果父接口有 1 个,子接口就不能再加了。

Q3:Comparatorcompareequals 两个抽象方法,为什么它还是函数式接口? A: equals 签名和 Object.equals 一致,而 Object 的方法不计入抽象方法数量——因为每个实现类都会从 Object 继承实现。所以 Comparator 实际只有 compare 一个"有效抽象方法"。


卡片 3|@FunctionalInterface —— 加了有什么好处?

Q:加不加 @FunctionalInterface,运行时行为有区别吗?

A: 没有区别。它纯粹是编译期的"看门狗":

  • 当你或同事在接口里无意加了第二个抽象方法 → 编译直接报错,而不是到运行时才发现 Lambda 绑定失败
  • 它也是一种文档信号——告诉调用者"这个接口是给 Lambda 用的,别乱加方法"

不加注解的坏处:可能写了 Lambda 发现编译不过,排查半天才知道接口里多了个方法。

🧠 钩子: 它是"编译器看门狗" + "代码文档",不加也行但有心智负担。


❌ 常见反例

Java
1// 看起来没问题,但编译通不过
2@FunctionalInterface
3interface Processor {
4    void process(int x);
5    // 三个月后,同事在这加了一行:
6    void process(String x);  // ❌ 立马编译报错,帮你拦截
7    // 如果没有 @FunctionalInterface,这里悄无声息地多了一个抽象方法,
8    // 原来 Lambda 写法全部炸掉,排查到天荒地老
9}

🔍 追问

Q1:能在一个非函数式接口上用 @FunctionalInterface 骗编译器吗? A: 不能,编译器真实检查抽象方法数量,不是看注解。

Q2:@FunctionalInterface 能被继承吗? A: 不会自动继承。子接口如果也只有一个抽象方法,可以加也可以不加。但建议加,保持一致性。


二、Lambda 表达式


卡片 4|Lambda 本质 —— 到底是不是匿名内部类的语法糖? ⭐ 高频

Q:Lambda 和匿名内部类是等价替换吗?底层实现有什么不同?

A:

不是等价替换。 虽然用起来像,底层完全不同:

Lambda匿名内部类
字节码不生成独立的 .class 文件,运行时用 invokedynamic + LambdaMetafactory 动态生成编译器生成 Outer$1.class
this 指向外层类实例匿名类自身
能用的接口只能是函数式接口任何接口/抽象类
变量遮蔽不能定义与外层同名的局部变量可以遮蔽

Lambda 的底层流程:

  1. 编译期生成一条 invokedynamic 指令
  2. 运行时 JVM 调用 LambdaMetafactory.metafactory()
  3. 通过 MethodHandle 动态生成一个实现类(非 .class 文件,而是内存中的字节码)

这样做的好处:不污染磁盘空间加载更快(不需要 ClassLoader 加载额外的类)、未来 JVM 可以持续优化而不需要重新编译源码

Java
1// 表面看差不多
2Runnable r1 = () -> System.out.println("Lambda");
3Runnable r2 = new Runnable() {
4    @Override
5    public void run() { System.out.println("匿名内部类"); }
6};
7
8// 但:
9// r1.getClass() → 类似 com.xxx.Main$$Lambda$1/0x000000...
10// r2.getClass() → 类似 com.xxx.Main$1
11// r1.getClass().getDeclaredFields() → 空
12// r2.getClass().getDeclaredFields() → 可能有外部类引用

🧠 钩子: 匿名内部类 = 编译期生成 class 文件;Lambda = 运行时动态捏一个,更轻更快。


❌ 常见反例

Java
1// ❌ 错误:用 Lambda 实现非函数式接口
2// 比如想用 Lambda 创建 Thread(其实可以,Runnable 是函数式接口)
3// 但如果自己写了个双方法接口:
4interface Worker {
5    void work();
6    void rest();  // 第二个抽象方法
7}
8
9// Worker w = () -> System.out.println("work");  // ❌ 编译错误!
10// 编译器不知道你实现的是 work 还是 rest
Java
1// ❌ 常见陷阱:Lambda 里 this 是外层类
2public class Outer {
3    void test() {
4        Runnable r = () -> System.out.println(this);  // this = Outer 实例
5        r.run();
6    }
7}
8// 在匿名内部类里,this = 匿名类本身

🔍 追问

Q1:Lambda 能序列化吗? A: 可以,但需要把 Lambda 表达式赋值给一个同时实现 Serializable 的接口类型。比如 Runnable r = (Runnable & Serializable) () -> ...

Q2:Lambda 表达式能访问外层方法的局部变量,那这个变量存哪了? A: 如果 Lambda 在局部变量作用域外执行(比如被传到另一个线程),局部变量早已被栈回收。所以 Lambda 拷贝了一份值到自己的字段里,这就是为什么要求 final——拷贝后原变量改动了,Lambda 看不到,会导致不一致。


卡片 5|Lambda 语法省略规则 —— 什么时候能省,什么时候不能?

Q:详细的省略规则有哪些?哪些场景容易踩坑?

A:

可以省略的三种情况:

Java
1// 1️⃣ 参数类型可以省略(编译器靠函数式接口推断)
2// 写完整版:
3Predicate<String> p1 = (String s) -> s.length() > 3;
4// 省略类型:
5Predicate<String> p2 = s -> s.length() > 3;
6
7// 2️⃣ 单参数可以省略括号
8Consumer<String> c = s -> System.out.println(s);  // ✅
9// 但 0 参数或 2+ 参数必须加括号:
10Supplier<String> sup = () -> "hello";       // ✅ 必须括号
11BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;  // ✅ 必须括号
12
13// 3️⃣ 单行函数体可以省略 {} 和 return
14Function<Integer, String> f1 = i -> { return String.valueOf(i); };  // 完整版
15Function<Integer, String> f2 = i -> String.valueOf(i);              // 省略版 ✅
16
17// 多行必须写 {} 和 return:
18Function<Integer, String> f3 = i -> {
19    System.out.println("debug: " + i);  // 多行
20    return String.valueOf(i);
21};

🧠 钩子: 类型可省,单参可省括号,单行可省花括号 return。多行一个都不能省。


❌ 常见反例

Java
1// ❌ 错误:多行却省略 return
2Function<Integer, String> f = i -> {
3    String s = "prefix_" + i;
4    s;  // ❌ 这是表达式语句,不是 return!
5};
6// 编译器会报:not a statement 或 missing return statement
7
8// ❌ 错误:0 参数省略了括号
9// Supplier<String> s = -> "hello";  // ❌ 不行,必须 ()
10Supplier<String> s = () -> "hello";  // ✅
11
12// ❌ 错误:参数类型一部分省一部分不省
13// BiFunction<Integer, Integer, Integer> add = (int a, b) -> a + b;  // ❌ 要么全写要么全省
14BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;  // ✅
Java
1// ❌ 常见陷阱:省略花括号时无意中改变了语义
2// 你以为这样是 { print; return; }:
3Consumer<String> c = s -> System.out.println(s); // ✅ 正确
4
5// 但如果写成:
6// Runnable r = () -> System.out.println("hello");  // 这其实没有 return
7// 跟 Runnable 的 void run() 刚好匹配,所以可以
8// 但如果是 Function,必须有 return 值

🔍 追问

Q1:为什么"类型全写"和"类型全省"不能混着来? A: 这是 Java 语言规范的设计决定——类型推断是"全有或全无"的,要么根据函数式接口全部推断,要么你全部显式声明。部分推断会引入模糊性,比如 (Integer a, b)b 的类型应该是什么?从上下文推断一个却不推断另一个,规则会更复杂。

Q2:Lambda 参数能用 var 吗? A: JDK11+ 支持 (var a, var b) -> a + b,效果和省略类型一样,但 var 可以在参数上加注解(如 (@NonNull var s) -> ...)。


卡片 6|Lambda vs 匿名内部类 —— 什么时候该用哪个?

Q:什么场景下必须用匿名内部类而不能用 Lambda?

A:

以下场景 Lambda 做不到

Java
1// 1️⃣ 接口有多个抽象方法(非函数式接口)
2// 必须用匿名内部类:
3WindowListener listener = new WindowListener() {
4    @Override public void windowOpened(WindowEvent e) {}
5    @Override public void windowClosing(WindowEvent e) {}
6    // ... 其他方法
7};
8
9// 2️⃣ 需要自己的实例状态(字段)
10// Lambda 里不能声明实例字段:
11Runnable r = new Runnable() {
12    private int count = 0;  // ✅ 匿名内部类可以
13    @Override
14    public void run() {
15        count++;
16    }
17};
18// Lambda 写法拿不到 this 的字段
19
20// 3️⃣ 需要 this 指向自己(比如用在递归或自引用场景)
21// 匿名内部类:this = 自己
22// Lambda:this = 外层类
23
24// 4️⃣ 创建抽象类的实例
25// 抽象类不能用于 Lambda(Lambda 只认接口):
26ActionListener l = new ActionListener() { // ✅ 匿名内部类
27    @Override
28    public void actionPerformed(ActionEvent e) {}
29};

🧠 钩子: 多方法接口 / 需要自己的字段 / 需要自己的 this / 抽象类 → 只能用匿名内部类。


❌ 常见反例

Java
1// ❌ 经典错误:在 Lambda 里用 this 引用自己
2// 场景:一个按钮点击后把自己 disable
3Button btn = new Button();
4btn.setOnAction(e -> {
5    // this 不是 btn!this 是外层类!
6    // this.setDisable(true);  // ❌ 编译错误 or 错对象
7    btn.setDisable(true);     // ✅ 直接引用外部变量
8});
9
10// 对比匿名内部类:
11btn.setOnAction(new EventHandler<ActionEvent>() {
12    @Override
13    public void handle(ActionEvent event) {
14        // this 指向匿名内部类,也不是 btn!
15        // 所以这个场景下两者 this 都不指向 btn,需要显式引用外部变量
16    }
17});

🔍 追问

Q1:Lambda 和匿名内部类哪个性能更好? A: Lambda 通常更好:不生成 .class 文件(减少类加载开销),invokedynamic 允许 JVM 未来优化。匿名内部类编译后就固定了。但差异在绝大多数场景微乎其微,更关键的是可读性。

Q2:为什么 Lambda 不能有自己独立的 this A: 设计目标就是让它感觉像"普通代码块"而不是"一个对象"。如果 this 指向 Lambda 自身,访问外层类的成员就要写 Outer.this.field,这和设计哲学"简洁"矛盾。


三、方法引用 ::


卡片 7|四种方法引用 —— 一招吃透 ::

Q::: 的四种形式分别怎么用?背后的规则是什么?

A:

方法引用的核心规则:参数列表和返回值类型必须与函数式接口的抽象方法匹配。

#形式语法Lambda 等价写法参数怎么来的
静态方法ClassName::staticMethod(x) -> ClassName.staticMethod(x)全部参数透传
特定实例instance::method(x) -> instance.method(x)全部参数透传
类的实例方法ClassName::instanceMethod(obj, x) -> obj.instanceMethod(x)第一个参数变成调用者
构造器ClassName::new(x) -> new ClassName(x)全部参数透传

第③种最反直觉,多给一个例子:

Java
1// ③ 类的实例方法引用:第一个参数成为方法调用者
2// 函数式接口:BiFunction<String, String, Integer>
3// 抽象方法:Integer apply(String t, String u)
4// 方法引用:String::indexOf
5// 效果:(t, u) -> t.indexOf(u)   ← t 成了"调用者",u 是参数
6
7BiFunction<String, String, Integer> f = String::indexOf;
8int result = f.apply("Hello", "el");  // = "Hello".indexOf("el") = 1
Java
1// 另一个经典例子
2// Stream 中:names.stream().map(String::toUpperCase)
3// map 的参数是 Function<String, String>,即 String apply(String s)
4// String::toUpperCase 是"类的实例方法引用",
5// 接收一个 String(即 s),调用 s.toUpperCase()
6List<String> upper = names.stream().map(String::toUpperCase).collect(...);

🧠 钩子: ③最特殊——"类的实例方法引用",第一个参数变调用者,后面参数传入方法。


❌ 常见反例

Java
1// ❌ 错误:参数不匹配
2// Predicate<String> 的 test 方法:boolean test(String s)
3// String::isEmpty 是 boolean isEmpty()(无参数)
4// 签名匹配 → 可以用!因为 String::isEmpty 的隐含参数就是调用者本身
5Predicate<String> p = String::isEmpty;  // ✅ 其实合法!等价于 s -> s.isEmpty()
6
7// ❌ 错误:返回类型不匹配
8// Consumer<String> 的 accept 方法:void accept(String s)
9// String::toUpperCase 返回 String,不是 void
10// Consumer<String> c = String::toUpperCase;  // ❌ 编译错误!
11// 应该用 Function<String, String> f = String::toUpperCase;
Java
1// ❌ 错误:构造器引用匹配错了
2// Supplier<Person> 的 get():无参数,返回 Person
3Supplier<Person> s = Person::new;  // ✅ 调用 Person() 无参构造
4
5// Function<String, Person> 的 apply(String):一个参数,返回 Person
6Function<String, Person> f = Person::new;  // ✅ 调用 Person(String name)
7
8// BiFunction<String, Integer, Person>:两个参数
9BiFunction<String, Integer, Person> bf = Person::new;  // ✅ 调用 Person(String, Integer)
10// 构造函数的重载选择完全由函数式接口的参数列表决定!

🔍 追问

Q1:为什么 System.out::println 能匹配 Consumer<String> A: Consumer<String> 的抽象方法是 void accept(String s)。而 System.outPrintStream 实例,println(String) 接收 String 返回 void。完美匹配。"特定实例"形式自动把 accept 的参数传给 println。

Q2:方法引用可以串联吗? A: 不能直接 A::b::c。但可以组合函数式接口来实现:Function<String, String> f = ((Function<String, Integer>) Integer::parseInt).andThen(String::valueOf)

Q3:如果构造器有两个版本(有参/无参),ClassName::new 怎么选? A: 由函数式接口决定。Supplier<T> → 无参构造;Function<A, T> → 单参构造;BiFunction<A, B, T> → 双参构造。编译器自动推断。


四、Lambda 作用域


卡片 9|final 限制 —— 为什么不能改局部变量? ⭐ 高频

Q:详细的 final 限制是什么?"effectively final" 怎么判断?

A:

规则: Lambda 体内引用外部局部变量时,该变量必须是 finaleffectively final(虽然没有显式声明 final,但赋值后从未改变)。

"effectively final" 的判断标准很简单:

  • 变量只被赋值一次(包括声明时赋值)→ 符合
  • 声明后又被修改 → 不符合,即使从没在 Lambda 中使用也会报错(但 Lambda 不用它时不会报,编译器只在引用处检查)
Java
1// ✅ effectively final
2int x = 10;
3Runnable r = () -> System.out.println(x);
4
5// ❌ 不是 effectively final
6int y = 10;
7y = 20;  // 重新赋值了
8// Runnable r2 = () -> System.out.println(y);  // ❌ 编译错误

为什么有这个限制?
Lambda 可能比它所处的栈帧活得久(比如被提交到线程池)。当 Lambda 执行时,原来的局部变量早已被栈回收。所以 Java 在 Lambda 对象创建时拷贝一份值到 Lambda 的字段里。如果原变量后续被改动,拷贝的值和原变量就不一致了——这就是"必须 final"的根本原因:拷贝后原变量不改,一致性才能保证。

🧠 钩子: 局部变量会在 Lambda 创建时被拷贝走。拷贝的值和原来的变量必须保持一致 → 原变量不能改 → final。


❌ 常见反例

Java
1// ❌ 经典错误:在循环里用 Lambda
2for (int i = 0; i < 3; i++) {
3    // Runnable r = () -> System.out.println(i);  // ❌ 编译错误!
4    // i 不是 effectively final(i++ 了两次)
5}
6
7// ✅ 正确做法:拷贝一份
8for (int i = 0; i < 3; i++) {
9    int copy = i;  // copy 是 effectively final
10    Runnable r = () -> System.out.println(copy);  // ✅
11}
Java
1// ❌ 错误:数组元素的引用问题(初学者最常踩的坑)
2int[] sum = {0};
3Arrays.asList(1, 2, 3).forEach(n -> sum[0] += n);  // ✅ 合法!
4// 等等,不是说不能改吗?注意:sum 这个引用没有变(仍是同一个数组),
5// 变的只是数组的内容。final 限制的是"变量本身",不是"变量指向的对象内部状态"。
6// 这其实是个坏实践——如果你在并行流里这么做,就会出现竞态条件!
Java
1// ❌ 并行流中的共享可变状态(实际开发中出 bug 的常见原因)
2int[] total = {0};
3list.parallelStream().forEach(n -> total[0] += n);
4// ⚠️ 竞态条件!结果不确定。正确做法:用 reduce 或 AtomicInteger

🧠 钩子: final 限制的是变量引用不能变,不是指向的对象不能变。但改对象内部状态在并行流里是危险的。


🔍 追问

Q1:为什么 Lambda 能改成员变量却没有这个限制? A: 成员变量和对象的生命周期绑定,Lambda 只要持有 this 引用就能通过它读/写堆上的成员变量。不存在"栈被回收"的问题。

Q2:如果 Lambda 不在当前线程执行,改了成员变量为什么没问题? A: 有可能有问题(竞态条件),但这是并发安全问题,不是语言学上的限制。局部变量的 final 限制是语言规范层面的强制安全;成员变量的并发安全由开发者自己负责(加锁、volatile、AtomicXXX)。


卡片 10|成员变量 vs 局部变量 —— 为什么区别对待?

Q:详细对比 Lambda 中访问成员变量和局部变量的不同?

A:

Java
1public class ScopeDemo {
2    private int member = 10;           // 成员变量
3    private static int staticVar = 20;  // 静态变量
4
5    public void test() {
6        int local = 30;  // 局部变量
7
8        Runnable r = () -> {
9            System.out.println(member);     // ✅ 可读
10            member++;                        // ✅ 可写(但要处理并发问题)
11            System.out.println(staticVar);  // ✅ 可读
12            staticVar++;                     // ✅ 可写
13            System.out.println(local);      // ✅ 可读
14            // local++;                      // ❌ 不可写!不是 effectively final
15        };
16    }
17}

根本原因: 变量存在哪决定了规则。

  • 局部变量 = 栈上 → Lambda 拷贝值到自己字段,原变量必须 final 保证一致性
  • 成员变量 = 堆上 → Lambda 通过 this 引用直接访问,不拷贝,没有一致性问题

🧠 钩子: 位置决定命运——栈上的(局部)final,堆上的(成员)随便。


❌ 常见反例

Java
1// ❌ 混淆:以为"静态变量也在方法区,为啥能改"
2// 静态变量不在栈上,在方法区(或元空间),Lambda 通过 Class 直接访问,
3// 不涉及拷贝,不违反一致性逻辑。

五、内置四大函数式接口


卡片 12|四大核心接口 —— 别再背参数名了 ⭐ 高频

Q:四大接口各自是什么?方法名和签名?

A:

接口抽象方法签名中文记法使用场景
Predicate<T>boolean test(T t)T → booleanfilter, 条件判断
Function<T,R>R apply(T t)T → Rmap, 类型转换
Supplier<T>T get()() → T工厂, 懒加载
Consumer<T>void accept(T t)T → voidforEach, 副作用
Java
1// 每个接口一行实战代码
2Predicate<String> isEmpty = String::isEmpty;            // 断言
3Function<String, Integer> len  = String::length;        // 转换
4Supplier<Double> random       = Math::random;           // 生产
5Consumer<String> printer      = System.out::println;    // 消费

🧠 钩子: Predicate = test 断;Function = apply 转;Supplier = get 生;Consumer = accept 消。


❌ 常见反例

Java
1// ❌ 错误:Function 用错了方向
2// Function<Integer, String> 意思是:接收 Integer,返回 String
3Function<Integer, String> f = String::valueOf;  // ✅ int → String
4// 反过来就错了:
5// Function<String, Integer> f2 = String::valueOf;  // ❌ String → String? 不对!valueOf 返回 String
6
7// ❌ 错误:把 Consumer 当 Function 用
8// Consumer<String> c = String::toUpperCase;  // ❌ toUpperCase 返回 String,不是 void
9Consumer<String> c = System.out::println;     // ✅ println 返回 void

🔍 追问

Q1:为什么 Supplier 不叫 Producer A: 命名来自函数式编程传统。"Supplier" is a common name for a function that supplies values without taking any input. Java 采纳了这个命名传统。Guava 里类似接口叫 Supplier,JDK8 沿用了。

Q2:四大接口各自的变体有哪些? A: 非常多在 java.util.function 包里:

变体类型例子说明
双参数版本BiFunction<T,U,R> BiPredicate<T,U> BiConsumer<T,U>接收两个参数
同类型算子UnaryOperator<T>(= Function<T,T>BinaryOperator<T>(= BiFunction<T,T,T>输入输出同类型
原始类型特化IntPredicate LongFunction<R> DoubleConsumer避免自动装箱
双原始类型ToIntBiFunction<T,U> ObjIntConsumer<T>混合

面试高频UnaryOperator<T>BinaryOperator<T>,Stream 的 reduce 就用了 BinaryOperator


卡片 14|compose 和 andThen —— 方向别反了

Q:composeandThen 到底谁先谁后?有没有直观的记忆法?

A:

Java
1Function<Integer, Integer> f = x -> x + 1;   // f(x) = x + 1
2Function<Integer, Integer> g = x -> x * 2;   // g(x) = x * 2
3
4// compose:参数函数先执行,自己的函数后执行
5Function<Integer, Integer> h1 = g.compose(f);
6// h1(x) = g(f(x)) = g(x+1) = (x+1)*2
7// compose 里 f 在"前"(先跑)
8
9// andThen:自己的函数先执行,参数函数后执行
10Function<Integer, Integer> h2 = g.andThen(f);
11// h2(x) = f(g(x)) = f(x*2) = x*2 + 1
12// andThen 里 f 在"后"(后跑)
13
14// 数值验证:
15h1.apply(3);  // (3+1)*2 = 8
16h2.apply(3);  // 3*2+1   = 7

记忆法:

  • compose = "组合到前面" → 参数函数先跑(前 → 后:compose参数 → this)
  • andThen = "然后" → 参数函数后跑(前 → 后:this → andThen参数)

一句话:compose 把参数塞前面,andThen 把参数塞后面。

Java
1// 链式模拟数学:h(x) = sqrt( abs(x) ) + 1
2// 即 h(x) = add1( sqrt( abs(x) ) )
3// 函数分解:abs → sqrt → add1
4Function<Double, Double> h =
5    ((Function<Double, Double>) Math::abs)    // abs
6    .andThen(Math::sqrt)                       // 然后 sqrt
7    .andThen(x -> x + 1);                      // 然后 +1
8// andThen 可以一直链下去,compose 也可以但不太自然

🧠 钩子: compose = 把参数函数塞前面先跑;andThen = 把参数函数塞后面后跑。


❌ 常见反例

Java
1// ❌ 常见错误:方向搞反了
2// 需求:先 trim 再转大写
3Function<String, String> trim = String::trim;
4Function<String, String> upper = String::toUpperCase;
5
6Function<String, String> wrong = trim.andThen(upper);     // ✅ 正确:先 trim 后 upper
7Function<String, String> also = upper.compose(trim);      // ✅ 也正确:先 trim 后 upper
8// 两者结果一样:trim→upper
9
10// 混淆时刻:
11Function<String, String> bug = upper.andThen(trim);       // 先 upper 后 trim(可能不是你想要的)
12// " hello " → upper → " HELLO " → trim → " HELLO"?不对 trim 只去首尾空格,先去空格再大写才有意义

六、Optional


卡片 15|Optional 本质 —— 不止是 NPE 克星 ⭐ 高频

Q:Optional 到底解决了什么?和 @Nullable 注解有什么不同?

A:

Optional 解决的核心问题不是"NPE 不会再出现",而是把"可能为空"这个事实暴露在类型签名里,强迫调用者处理它

Java
1// 传统写法:返回值类型看不出是否可能为 null
2public User findUser(String name) {
3    // 返回 null 还是空 User?调用方不看文档就不知道
4    return null;
5}
6
7// Optional 写法:签名自己说清楚了 —— "结果有可能没有"
8public Optional<User> findUser(String name) {
9    return Optional.ofNullable(db.get(name));
10}
11// 调用方:必须显式处理空值,否则拿不到里面的 User
12User user = findUser("张三").orElse(User.GUEST);

Optional vs @Nullable

  • @Nullable 只是注解/文档,不写检查代码照样 NPE
  • Optional 是编译器/运行时强制——你必须调用 orElse / ifPresent / get 才能拿到值

🧠 钩子: Optional 不是免死金牌(of(null) 照样炸),而是"我可能为空,你看着办"的强制提醒。


❌ 常见反例

Java
1// ❌ 最致命的反模式:把 Optional 本身当 null 用
2public Optional<User> findUser(String name) {
3    if (name == null) return null;  // ❌❌❌ 绝对不要这样!
4    // Optional 方法返回 null 等于双重 null,调用方的 orElse 全白费
5    return Optional.empty();  // ✅ 这才是对的
6}
7
8// ❌ 反模式:用 isPresent() + get() 代替 orElse
9Optional<User> opt = findUser("张三");
10if (opt.isPresent()) {          // ❌ 这是传统 null 检查的翻版,没发挥 Optional 优势
11    User user = opt.get();      // ❌ isPresent + get = 和 if(x != null) 没区别
12    // ...
13}
14// ✅ 正确:
15findUser("张三").ifPresent(user -> { /* ... */ });
16// 或
17User user = findUser("张三").orElse(User.GUEST);
18
19// ❌ 反模式:Optional 作为方法参数
20public void process(Optional<User> user) {  // ❌ 别这样设计 API
21    // 调用方传来的 Optional 可能是 null → 你的代码就炸了
22}
23// ✅ Optional 只应该作为返回值类型,不推荐作为参数或字段
Java
1// ❌ 经典错误:of() vs ofNullable()
2Optional<String> opt1 = Optional.of(null);       // ❌ NullPointerException!
3Optional<String> opt2 = Optional.ofNullable(null); // ✅ 返回 Optional.empty()
4// 规则:确定不为空用 of;不确定用 ofNullable

🔍 追问

Q1:为什么 Optional.get() 被认为是不安全的? A: 因为如果 Optional 是 emptyget() 会抛 NoSuchElementException,本质上把 NPE 换成了另一个异常。安全做法是永远搭配 orElse / orElseGet / orElseThrow

Q2:Optional 的 mapflatMap 区别? A: 和 Stream 一样:

Java
1Optional<User> user = findUser("张三");
2// map:User → String → Optional<String>
3Optional<String> name = user.map(User::getName);
4// flatMap:当转换函数自身也返回 Optional 时,避免 Optional<Optional<T>>
5// flatMap:User → Optional<Address> → Optional<Address>(拍平)
6Optional<Address> addr = user.flatMap(User::getAddress);
7// 如果 User.getAddress() 返回 Optional<Address>,
8// 用 map 会得到 Optional<Optional<Address>>,用 flatMap 得到 Optional<Address>

Q3:orElseorElseGet 有什么区别? A: orElse 是急切的——即使 Optional 有值,默认值也会被创建;orElseGet 是惰性的——只有缺失时才调用 Supplier。如果默认值创建很贵,用 orElseGet

Java
1// orElse:不管 Optional 有没有值,expensiveDefault() 都会执行
2String s1 = opt.orElse(expensiveDefault());  // ❌ 浪费
3
4// orElseGet:只有缺失时才执行 expensiveDefault()
5String s2 = opt.orElseGet(() -> expensiveDefault());  // ✅

卡片 16|Optional API 全景图

Q:Optional 完整 API 梳理?

A:

Java
1// ========== 创建 ==========
2Optional.empty()              // 空 Optional
3Optional.of(value)            // 非空值(null 则 NPE)
4Optional.ofNullable(value)    // 允许 null,null 变成 empty
5
6// ========== 取值 ==========
7opt.get()                     // ⚠️ 不安全,empty 时抛异常
8opt.orElse(default)           // 有值返回值,否则返回默认值
9opt.orElseGet(() -> ...)      // 惰性默认值
10opt.orElseThrow()             // 有值返回值,否则抛 NoSuchElementException
11opt.orElseThrow(() -> new X)  // 自定义异常
12
13// ========== 判空执行 ==========
14opt.isPresent()               // 有值返回 true
15opt.isEmpty()                 // JDK11+:空返回 true
16opt.ifPresent(v -> ...)       // 有值则消费
17opt.ifPresentOrElse(v -> ..., () -> ...)  // JDK9+:有值/无值双处理
18
19// ========== 链式转换 ==========
20opt.map(v -> ...)             // 有值时转换,返回 Optional
21opt.flatMap(v -> ...)         // 和 map 一样但自动拍平
22opt.filter(v -> condition)    // 有值且满足条件才保留,否则 empty
23
24// ========== 流转换(JDK9+)==========
25opt.stream()                  // 变成 0 或 1 个元素的 Stream

🧠 钩子: 创建三件套 + 取值三件套 + map/flatMap/filter 链 = 必备技能。


七、Stream 流


卡片 17|Stream 本质 —— 不是数据是管道 ⭐ 高频

Q:Stream 的本质是什么?三大特性为什么这么设计?

A:

Stream 不是数据结构,是对数据源的计算流程描述。本质是"声明式流水线":

Java
1names.stream()                 // 获取数据源
2    .filter(n -> n != null)    // 描述步骤1:去空
3    .map(String::toUpperCase)  // 描述步骤2:转大写
4    .sorted()                  // 描述步骤3:排序
5    .collect(toList());        // 触发执行,输出结果

三大核心特性及其设计原因:

① 不存数据 —— Stream 只是数据的"视图",不会复制一份。内存开销极小。

② 惰性求值 —— 中间操作(filter、map)只是"记账",不真执行。只有终端操作(collect、count)才触发全部计算。好处:

  • 中间操作可以融合filter→map 可以在一次遍历中完成,不用先过滤完再映射
  • 可以短路findFirst 找到第一个就停止,不用处理全部元素
  • 无限流成为可能:Stream.iterate(0, i->i+1).limit(10) — limit 之前不用真的生成无限个

③ 一次性消费 —— 流用完就没了,不能复用。像"读完的文件流"一样。需要复用就重新从数据源获取。

🧠 钩子: Stream = 不存数据的惰性流水线,用完即弃。


❌ 常见反例

Java
1// ❌ 经典错误:复用已消费的 Stream
2Stream<String> stream = list.stream().filter(s -> s.length() > 3);
3long count = stream.count();        // ✅ 第一次消费
4// long count2 = stream.count();    // ❌ IllegalStateException: stream has already been operated upon or closed
5
6// ✅ 正确:需要多次用时重新获取
7long count1 = list.stream().filter(s -> s.length() > 3).count();
8long count2 = list.stream().filter(s -> s.length() > 3).count();
9
10// ❌ 错误:在 Stream 操作中修改源数据
11List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
12// list.stream().forEach(s -> list.add(s + "!"));  // ⚠️ 可能 ConcurrentModificationException
13// Stream 不应该在操作过程中修改源数据,尤其在并行流中是灾难
Java
1// ❌ 常见误会:以为中间操作改了源集合
2List<String> list = new ArrayList<>(Arrays.asList("b", "a", "c"));
3list.stream().sorted().collect(toList());  // 返回新 List
4System.out.println(list);  // [b, a, c] —— 原集合没变!sorted 只是视图排序

🔍 追问

Q1:Stream 能不能无限大? A: 可以,用 Stream.generate()Stream.iterate() 创建无限流。但必须配合 limit()findFirst() 等短路终端操作,否则会无限运行。

Java
1// 斐波那契无限流(只取前10个)
2Stream.iterate(new int[]{0, 1}, f -> new int[]{f[1], f[0] + f[1]})
3    .limit(10)
4    .map(f -> f[0])
5    .forEach(System.out::println);

Q2:StreamIntStream / LongStream / DoubleStream 有什么区别? A: 原始类型特化版本,避免自动装箱开销。IntStream 多了 sum()average()range()summaryStatistics() 等数值专用方法。


卡片 18|中间 vs 终端操作 —— 记住"懒"和"点火" ⭐ 高频

Q:所有中间和终端操作的完整清单?

A:

中间操作(返回 Stream,惰性)特点终端操作(触发,输出结果)输出类型
filter(Predicate)过滤forEach(Consumer)void
map(Function)转换forEachOrdered(Consumer)void(保证顺序)
flatMap(Function→Stream)转换+拍平collect(Collector)R
distinct()去重toList() (JDK16+)List<T>
sorted()排序count()long
sorted(Comparator)排序min(Comparator)Optional<T>
peek(Consumer)调试偷看max(Comparator)Optional<T>
limit(long)截断anyMatch(Predicate)boolean
skip(long)跳过allMatch(Predicate)boolean
takeWhile(Pred) (JDK9+)满足则取noneMatch(Predicate)boolean
dropWhile(Pred) (JDK9+)满足则丢findFirst()Optional<T>
mapToInt/mapToLong/mapToDouble转原始类型findAny()Optional<T>
reduce(BinaryOperator)Optional<T>
reduce(identity, BinaryOp)T
reduce(identity, BiFunc, BinaryOp)U

🧠 记忆法: 返回 Stream 的 = 中间;返回非 Stream 的 = 终端。peek 是个例外(返回 Stream 但通常有副作用,只用于调试)。


❌ 常见反例

Java
1// ❌ 经典错误:没有终端操作,什么都不执行
2list.stream()
3    .filter(s -> { System.out.println("filter: " + s); return true; })
4    .map(s -> { System.out.println("map: " + s); return s; });
5// 控制台空空如也!因为没加终端操作,整个管道没被"点火"
6
7// ✅ 加个终端操作:
8list.stream()
9    .filter(s -> { System.out.println("filter: " + s); return true; })
10    .map(s -> { System.out.println("map: " + s); return s; })
11    .collect(toList());  // ← 点火!现在控制台有输出了
Java
1// ❌ 错误:终端操作后还想继续流操作
2Stream<String> s = list.stream();
3s.filter(...).collect(toList());  // ✅
4// s.map(...)                      // ❌ 流已经消费了

🔍 追问

Q1:forEachforEachOrdered 有什么区别? A: 在顺序流中没区别。在并行流中,forEach 不保证顺序(为了效率),forEachOrdered 严格按原始顺序。但 forEachOrdered 会牺牲并行性能。

Q2:findFirstfindAny 有什么区别? A: 顺序流中没区别。并行流中,findFirst 返回第一个(限制并行效率),findAny 返回任意一个(更高效)。如果只关心"有没有"而不关心"第几个",用 findAny


卡片 19|sorted 的坑 —— 为什么它不是真正的流式操作?

Q:为什么 sorted 是"有状态"中间操作?还有哪些操作有状态?

A:

**"有状态"**意味着操作需要记住已经见过的所有元素才能继续。对于 sorted,你必须看完所有元素才知道最小的(或第一个排序后的)是什么。

Java
1// 直观理解:排序必须全部看完才能确定第一个
2// 如果给你 1, 5, 3, 2 四个数
3// filter(x > 2):来一个判一个,x=1 直接丢 → 流式 ✅
4// sorted():看完 1,5,3,2 才知道排序后是 1,2,3,5 → 非流式 ❌(得全量缓存)

有状态的中间操作清单:

  • sorted() / sorted(Comparator) — 全量排序
  • distinct() — 记住所有见过的元素才能去重
  • limit() — 无状态(直接数就行)
  • skip() — 有状态(得数过去多少个)
  • takeWhile / dropWhile — 无状态(条件一破坏就停止/开始)

🧠 钩子: sorted 和 distinct 都要"全量记忆",大数据量可能 OOM。


❌ 常见反例

Java
1// ⚠️ 性能陷阱:大文件行排序
2// 如果文件有 10GB,stream 里 sorted() 会把所有行读到内存 → OOM
3Files.lines(Paths.get("huge.log"))   // 惰性读取
4    .sorted()                         // ❌ 读进内存,可能 OOM
5    .forEach(System.out::println);
6
7// ✅ 正确:大文件排序用外部排序,或数据库排序,或先 limit 再 sorted
Java
1// ❌ 顺序误区:先 sorted 再 filter vs 先 filter 再 sorted
2list.stream()
3    .sorted()       // 排序全部
4    .filter(...)    // 过滤掉大部分
5    .collect(...);
6// 不如:
7list.stream()
8    .filter(...)    // 先过滤掉大部分
9    .sorted()       // 只排序剩下的 ← 更高效!
10// 原则:先 filter 缩小数据集,再做 sorted / distinct 等有状态操作

八、并行流


卡片 21|并行流原理 —— ForkJoinPool 是怎么工作的? ⭐ 高频

Q:并行流底层用了什么?分片策略是怎样的?

A:

底层: parallelStream() 使用 ForkJoinPool.commonPool()(公共线程池)。线程数默认 = Runtime.getRuntime().availableProcessors() - 1(至少 1)。

流程:

Java
1// 列表:[1, 2, 3, 4, 5, 6, 7, 8]
2list.parallelStream()
3    .map(x -> x * 2)
4    .reduce(0, Integer::sum);
5
6// 内部大致流程:
7// Step 1: Fork(拆分)
8//   [1,2,3,4,5,6,7,8] → [1,2,3,4] + [5,6,7,8]
9//   → [1,2] + [3,4]   +   [5,6] + [7,8]
10// Step 2: Process(并行处理)
11//   四个线程分别处理 [1,2] [3,4] [5,6] [7,8]
12//   → (2,4) (6,8) (10,12) (14,16)
13// Step 3: Join(合并)
14//   → (2+4+6+8) + (10+12+14+16) = 20 + 52 = 72

分片策略: ArrayList 用数组索引分片(高效),LinkedListHashSet 分片效率低(遍历才知道大小),不适合并行流。

🧠 钩子: ForkJoinPool 公共池,默认 CPU 核心数-1 线程。ArrayList 适合并行,LinkedList 不适合。


❌ 常见反例

Java
1// ❌ 错误:在并行流内使用非线程安全的集合
2List<Integer> results = new ArrayList<>();  // 不是线程安全的!
3list.parallelStream()
4    .map(x -> x * 2)
5    .forEach(results::add);   // ❌ 竞态条件!可能丢数据或抛异常
6// ✅ 用 collect:
7List<Integer> safe = list.parallelStream()
8    .map(x -> x * 2)
9    .collect(toList());        // ✅ 线程安全
10// ✅ 或用线程安全容器:
11List<Integer> results = Collections.synchronizedList(new ArrayList<>());
12
13// ❌ 错误:并行流里用了有状态的 Lambda
14int[] sum = {0};  // ❌ 竞态条件
15list.parallelStream().forEach(n -> sum[0] += n);
Java
1// ❌ 错误:在公共 ForkJoinPool 中执行阻塞操作
2list.parallelStream().forEach(item -> {
3    // Thread.sleep(1000);  // ❌ 占用公共线程池线程,影响全局
4});
5// 公共线程池被阻塞 → JVM 中所有用到 parallelStream 的地方都受影响

🔍 追问

Q1:怎么自定义并行流的线程数? A: 默认用公共池无法直接改。但可以这样:

Java
1ForkJoinPool customPool = new ForkJoinPool(4);
2customPool.submit(() ->
3    list.parallelStream().forEach(...)
4).get();
5// 这会使用自定义池而不是公共池

Q2:并行流的元素顺序有保证吗? A: forEach 不保证,forEachOrdered 保证但影响性能。collect(toList()) 输出顺序与输入一致(Collector 保证顺序)。findAny 不保证,findFirst 保证。

Q3:哪些数据源适合并行流? A: ArrayList、数组、IntStream.range → 非常适合(O(1) 随机访问分片);HashSet、TreeSet → 一般(有点分片开销);LinkedList、Stream.iterate → 不适合(很难分片)。


九、Map 新特性


卡片 23|Map 新方法 —— 让你告别冗长的 if-else

Q:JDK8 Map 新增方法的使用场景和最佳实践?

A:

putIfAbsent — "不存在时才放"

Java
1// 旧写法(3行)
2if (!map.containsKey(key)) {
3    map.put(key, value);
4}
5// 新写法(1行)
6map.putIfAbsent(key, value);

computeIfAbsent — "不存在时计算并放入"(最高频!)

Java
1// 经典场景:分组 + 计数 / 分组 + 列表
2Map<String, List<User>> byCity = new HashMap<>();
3for (User u : users) {
4    byCity.computeIfAbsent(u.getCity(), k -> new ArrayList<>())
5          .add(u);
6}
7// 内部逻辑:如果 city 不在 map 中,执行 Lambda 创建 ArrayList,放入 map,返回它
8// 如果 city 已存在,直接返回已有的 List
9
10// 旧写法(需要 if + put,至少 5 行):
11List<User> list = byCity.get(u.getCity());
12if (list == null) {
13    list = new ArrayList<>();
14    byCity.put(u.getCity(), list);
15}
16list.add(u);

computeIfPresent — "存在时才计算更新"

Java
1// 场景:对购物车里的商品数量翻倍
2map.computeIfPresent(key, (k, v) -> v * 2);
3// 如果 key 存在,用新值替换;如果不存在,什么都不做

merge — "合并值"

Java
1// 场景:计数累加
2wordCounts.merge(word, 1, Integer::sum);
3// key 不存在 → 放 1
4// key 存在 → sum(old, 1) = old+1
5
6// 场景:合并两个 Map
7map2.forEach((k, v) -> map1.merge(k, v, Integer::sum));

⑤ 其他

Java
1map.getOrDefault(key, defaultValue);          // 不存在给默认值
2map.remove(key, value);                       // k 且 v 都匹配才删
3map.replace(key, value);                      // 存在则替换
4map.replace(key, oldValue, newValue);         // k 且 v 都匹配才替换

🧠 钩子: computeIfAbsent = 不存就算,computeIfPresent = 存了再算,merge = 合二为一。


❌ 常见反例

Java
1// ❌ 错误:computeIfAbsent 里又去改同一个 map(可能导致 ConcurrentModificationException)
2map.computeIfAbsent(key, k -> {
3    // map.put("other", "value");  // ❌ 别!在 compute 函数里修改 map 是不安全的
4    return "value";
5});
6
7// ❌ 错误:merge 的 remapping 函数返回 null
8map.merge(key, 1, (oldV, newV) -> null);
9// 如果 remapping 函数返回 null,会删除该 key!这是 merge 的特殊行为
10
11// ❌ 常见误解:putIfAbsent 返回什么?
12String old = map.putIfAbsent("key", "newValue");
13// 如果 key 已经存在 → 返回已有的 value
14// 如果 key 不存在 → 返回 null
15// 容易误以为返回"将要被 put 的值"

🔍 追问

Q1:computeIfAbsentputIfAbsent 核心区别? A: putIfAbsent 的值是已计算好的传进去;computeIfAbsent 的值是惰性计算的(Lambda),只在 key 不存在时才执行。如果值的创建很昂贵(比如新建一个大对象),用 computeIfAbsent 更省。

Q2:为什么 merge 的 remapping 函数返回 null 会删除 key? A: 这是刻意设计的:允许通过 merge 实现"如果计算结果为 null 则删除",省去额外的 remove 调用。如果不想删除,永远不要在 remapping 里返回 null。


卡片 24|Map 与 Stream —— Map 本身不流

Q:Map 为什么不直接支持 Stream?怎么在 Map 上用 Stream 操作?

A:

Map 不支持 .stream() 主要是语义问题——Stream 是"元素序列",而 Map 的"元素"是什么?key?value?entry?三者完全不同,没法用一个 stream() 同时表达。所以设计了三种视图:

Java
1Map<String, Integer> map = ...;
2
3// 三种视图各自都可以用 Stream:
4map.keySet().stream()      // Stream<String>  → 对 key 操作
5map.values().stream()      // Stream<Integer> → 对 value 操作
6map.entrySet().stream()    // Stream<Map.Entry<String, Integer>> → 对 key+value 操作
7
8// 实用例子:
9// 过滤出 value > 10 的 key
10List<String> keys = map.entrySet().stream()
11    .filter(e -> e.getValue() > 10)
12    .map(Map.Entry::getKey)
13    .collect(toList());
14
15// 把 Map 转成另一个 Map
16Map<String, Integer> doubled = map.entrySet().stream()
17    .collect(toMap(Map.Entry::getKey, e -> e.getValue() * 2));

🧠 钩子: Map 不直接流,但 keySet / values / entrySet 都能流。


十、Date API(java.time)


卡片 25|新日期 API —— 为什么老 Date 是"坏的"? ⭐ 高频

Q:java.util.Date 具体哪里不好?新 API 怎么解决了?

A:

老 Date 的罪状:

Java
1// 1️⃣ 可变 → 线程不安全
2Date d = new Date();
3d.setTime(1000);  // 原对象被改了!
4
5// 2️⃣ 偏移量诅咒:月份从 0 开始
6Date d = new Date(2024, 1, 1);  // 你以为 2024-01-01?
7// 实际是 3924-02-01!(年份 = 1900+2024,月份 1=2月)
8
9// 3️⃣ SimpleDateFormat 线程不安全!
10SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
11// 多线程共享同一个 sdf 实例 → 解析结果错乱,甚至抛异常
12
13// 4️⃣ 命名混乱
14Date d = new Date();          // 包含日期+时间,但叫"Date"
15Date d2 = new Date(0);        // 也是日期+时间

新 API 设计哲学:一切不可变 + 线程安全:

Java
1// 每个类只做一件事,名字说清楚
2LocalDate date = LocalDate.of(2024, 1, 1);    // 只有日期,月份终于从 1 开始
3LocalTime time = LocalTime.of(10, 30);         // 只有时间
4LocalDateTime dt = LocalDateTime.of(date, time); // 日期+时间
5ZonedDateTime zdt = ZonedDateTime.now();        // 带时区
6Instant instant = Instant.now();               // 机器时间戳
7
8// 所有修改都返回新对象
9LocalDate tomorrow = date.plusDays(1);  // date 本身没变
10LocalDate nextMonth = date.plusMonths(1); // 又一个新对象

🧠 钩子: 老API = 可变/月份0开头/线程不安全;新API = 不可变/1开头/线程安全。


❌ 常见反例

Java
1// ❌ 老代码常见错误:共享 SimpleDateFormat
2public class DateUtils {
3    private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd");
4    // 多线程共享 → ❌ 线程安全问题!!!
5
6    public static String format(Date d) {
7        return SDF.format(d);  // ❌ 并发调用会挂
8    }
9}
10
11// ✅ 新写法:
12public class DateUtils {
13    private static final DateTimeFormatter FMT =
14        DateTimeFormatter.ofPattern("yyyy-MM-dd");
15    // DateTimeFormatter 不可变,线程安全 ✅
16
17    public static String format(LocalDate d) {
18        return FMT.format(d);
19    }
20}
Java
1// ❌ 常见错误:LocalDateTime 不带时区,跨国业务直接用会出问题
2LocalDateTime dt = LocalDateTime.now();  // 拿的是服务器本地时间
3// 如果服务器在上海,用户在美国,这个时间对用户来说无意义
4
5// ✅ 跨时区用 ZonedDateTime 或 Instant
6Instant instant = Instant.now();  // UTC 时间,全球通用
7ZonedDateTime userTime = instant.atZone(ZoneId.of("America/New_York"));

🔍 追问

Q1:InstantLocalDateTime 什么关系? A: Instant = UTC 时间线上的一点(纯机器概念);LocalDateTime = 墙上时间(人类概念:2024-01-01 10:00),但不知道是哪个时区的。同一 Instant 在不同时区对应不同 LocalDateTime。

Q2:新 API 怎么和老的 Date/Calendar 互转? A:

Java
1// Date → Instant → LocalDateTime
2Date oldDate = new Date();
3LocalDateTime dt = oldDate.toInstant()
4    .atZone(ZoneId.systemDefault())
5    .toLocalDateTime();
6
7// LocalDateTime → Instant → Date
8Date newDate = Date.from(
9    LocalDateTime.now()
10        .atZone(ZoneId.systemDefault())
11        .toInstant()
12);

Q3:PeriodDuration 区别? A: Period = 基于日期的(年月日),比如"差 3 个月 5 天";Duration = 基于时间的(时分秒纳秒),比如"差 2 小时 30 分钟"。

Java
1Period period = Period.between(date1, date2);     // P3M5D
2Duration duration = Duration.between(time1, time2); // PT2H30M

十一、注解


卡片 27|重复注解 —— 什么时候真的需要它?

Q:@Repeatable 实际解决了什么问题?容器注解是什么?

A:

JDK8 之前,同一个注解同一个位置只能用一次:

Java
1// JDK7 及以前:想打多个 @Role → 必须用容器注解包起来
2@Roles({                       // 容器注解
3    @Role("admin"),
4    @Role("user")
5})
6public class User {}
7
8// JDK8:可以直接写多个同名注解
9@Role("admin")
10@Role("user")
11public class User {}

容器注解是什么?@Repeatable 用到的幕后容器——仍然存在,但被隐藏了。编译器在背后自动把多个 @Role 打包成 @Roles。用反射获取时:

Java
1// 反射获取时,直接用 getAnnotationsByType
2Role[] roles = User.class.getAnnotationsByType(Role.class);
3// 也能通过 getAnnotation(Roles.class) 获取容器,但不推荐

定义方式:

Java
1// 可重复的注解
2@Repeatable(Roles.class)  // 指定容器
3@interface Role {
4    String value();
5}
6
7// 容器注解
8@interface Roles {
9    Role[] value();  // 必须是 value(),且类型是重复注解的数组
10}

🧠 钩子: @Repeatable = 编译期去重,运行时靠反射 getAnnotationsByType。容器注解藏在幕后。


❌ 常见反例

Java
1// ❌ 错误:容器注解的 value() 属性的类型写错
2@Repeatable(Roles.class)
3@interface Role { String value(); }
4
5@interface Roles {
6    // ❌ 错误:
7    // String[] value();  // 必须是 Role[],不是其他类型
8    Role[] value();       // ✅ 正确
9}
10
11// ❌ 错误:容器注解的成员名必须是 value(编译器约定)
12@interface Roles {
13    // Role[] roles();  // ❌ 不能用其他名字
14    Role[] value();      // ✅ 必须叫 value
15}
Java
1// ❌ 常见错误:只获取容器注解而忽略了重复注解
2// Roles roles = cls.getAnnotation(Roles.class);  // JDK7 方式,能用但笨拙
3// ✅ 新 API:
4Role[] roles = cls.getAnnotationsByType(Role.class);  // JDK8 方式

🔍 追问

Q1:TYPE_USETYPE_PARAMETER 的区别? A:

  • TYPE_PARAMETER:只能标在类型参数声明上(如 <@NonNull T>
  • TYPE_USE:可以标在任何使用类型的地方(声明、new、cast、泛型参数等等),范围更广
Java
1// TYPE_PARAMETER:只能标类型参数的声明
2class Container<@MyAnnotation T> {}  // 标在 T 的声明上
3
4// TYPE_USE:标在类型使用的任何地方
5@MyAnnotation String name;                          // 字段声明
6String s = (@MyAnnotation String) obj;              // 强制转换
7new @MyAnnotation ArrayList<>();                    // 对象创建
8List<@MyAnnotation String> list;                    // 泛型参数

Q2:有什么实际场景会用到重复注解? A: 常见于权限/角色系统(一个方法需要多个角色)、校验框架(一个字段需要多种校验)、事件监听(一个方法处理多个事件类型)。Spring 的 @EventListener 用了 @Repeatable


📊 复习优先级 + 自检清单

⭐⭐⭐ 最高频(面试 90% 会问)

卡片自检问题能否 30 秒讲清?
4Lambda 底层不是语法糖,为什么?invokedynamic 怎么工作的?
9为什么局部变量要 final?"effectively final"的临界线在哪?
12四大函数式接口分别叫什么、方法名是什么、签名的箭头怎么写?
15Optional 解决的不是 NPE 消失,而是什么?of vs ofNullable 区别?
17Stream 三大特性?惰性求值为什么重要?中间操作什么时候才执行?
18至少说出 5 个中间操作和 5 个终端操作
21parallelStream 用的什么线程池?默认几个线程?怎么改?
25老 Date 四个缺点 + 新 API 两个核心优势?DateTimeFormatter vs SimpleDateFormat?

⭐⭐ 常考

卡片自检问题
1default 方法和抽象类有什么区别?什么时候用哪个?
2函数式接口定义?Comparator 为什么算函数式接口?
7:: 四种形式,第③种"类的实例方法引用"怎么理解?
16Optional 的 map vs flatMap,orElse vs orElseGet?
20reduce 的三个重载版本分别怎么用?
23computeIfAbsent 和 putIfAbsent 区别?merge 返回 null 会怎样?

⭐ 加分项

卡片自检问题
3 / 5 / 6 / 8 / 10 / 11 / 13 / 14 / 19 / 22 / 24 / 26 / 27至少过一遍,能说出大概即可

🔧 使用建议

  1. 先自检 —— 对着上面的清单,☐ 里打勾,找出真正的盲区
  2. 每张卡先看 Q,默写 A —— 写不出来才看答案,写完对比
  3. 每张卡的 ❌ 反例自己写一遍 —— 知道"什么不该写"比知道"什么该写"记得更牢
  4. 追问部分用来模拟面试 —— 让朋友/自己追问,逼出知识深度
  5. 配合源码跑 —— clone itstack-demo-jdk8,跑不通的地方就是没理解透的地方

继续阅读

推荐阅读

技术2026年6月10日饰品信号 V2 推倒重构技术方案技术2026年6月3日Java8新特性抽象类回顾 jdk1.8 之前,接口里只能做方法定义不能有方法的实现,因此通常会在抽象类里面实现默认的方法,一般这个默认的方法是抽象后公用的方法,不需要每一个继承者都去实现,只需调用即可,就像下面这样: java // 定义 public abstract class AFormula { abstract double calculate int a ; // 平方根 double sqrt i技术2026年6月3日实现多线程的方法是 1 种还是 2 种还是 4 种?实现多线程的方法是 1 种还是 2 种还是 4 种? 网上的说法 正确的说法 实现多线程的官方正确方法: 2 种。 Oracle 官网的文档说明 方法小结 方法一: 实现 Runnable 接口。 方法二: 继承 Thread 类。 代码示例 java package cn.xilikeli.threadcoreknowledge.createthreads; / <p 实现 Runnable 接
JDK1.8 新特性 · 复习手册 | 博击长空